﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using Fadd.Net;

namespace Fadd.Remoting
{
    internal class RemotingClient
    {
        private readonly Dictionary<int, Request> _requests = new Dictionary<int, Request>();
        private readonly IServiceResolver _serviceResolver;
        private readonly BinaryTransport<Packet> _transport;
        private int _transactionId;
        private EndPoint _remoteEndPoint;
        private Timer _reconnectTimer;

        /// <summary>
        /// Connected socket
        /// </summary>
        /// <param name="serviceResolver">Used to lookup services that should be invoked.</param>
        /// <param name="socket">Socket used for communication</param>
        public RemotingClient(IServiceResolver serviceResolver, Socket socket)
        {
            _serviceResolver = serviceResolver;
            _remoteEndPoint = socket.RemoteEndPoint;
            _transport = new BinaryTransport<Packet>(socket);
            _transport.Disconnected += OnDisconnect;
            _transport.ObjectReceived += OnObject;
        }

        private void HandleMethodResponse(Response response)
        {
            Request request;
            lock (_requests)
            {
                if (!_requests.TryGetValue(response.RequestId, out request))
                    return;
                _requests.Remove(response.RequestId);
            }

            request.Response = response;
            request.Event.Set();
        }


        /// <summary>
        /// Invoke a method at the other end.
        /// </summary>
        /// <param name="interfaceType">Component that the method exist in</param>
        /// <param name="methodName">Method to invoke</param>
        /// <param name="parameters">Method parameters</param>
        /// <exception cref="InvalidOperationException">Failed to get answer from the server.</exception>
        /// <remarks>
        /// <para>
        /// Use other Invoke method if the method is generic.
        /// </para>
        /// </remarks>
        public object Invoke(Type interfaceType, string methodName, params object[] parameters)
        {
            MethodInfo method = _serviceResolver.Resolve(interfaceType).GetType().GetMethod(methodName);
            if (method != null && method.IsGenericMethod)
                throw new InvalidOperationException("Method is generic, use InvokeGeneric instead.");

            var call = new MethodCall
                           {
                               InterfaceType = interfaceType,
                               MethodName = methodName,
                               Arguments = parameters,
                               RequestId = Interlocked.Increment(ref _transactionId)
                           };

            return InvokeMethodInternal(call);
        }


        /// <summary>
        /// Invoke a generic method at the other end.
        /// </summary>
        /// <param name="interfaceType">Component that the method exist in</param>
        /// <param name="methodName">Method to invoke</param>
        /// <param name="parameterTypes">All argument types, needed to find the </param>
        /// <param name="parameters">Method parameters</param>
        /// <exception cref="InvalidOperationException">Failed to get answer from the server.</exception>
        public object InvokeGeneric(Type interfaceType, string methodName, Type[] parameterTypes, object[] parameters)
        {
            var call = new MethodCall
                           {
                               InterfaceType = interfaceType,
                               MethodName = methodName,
                               GenericArgumentTypes = parameterTypes,
                               Arguments = parameters,
                               RequestId = Interlocked.Increment(ref _transactionId)
                           };

            return InvokeMethodInternal(call);
        }

        /// <exception cref="TargetInvocationException"><c>TargetInvocationException</c>.</exception>
        /// <exception cref="InvalidOperationException">Failed to get answer from remote end point.</exception>
        private object InvokeMethodInternal(MethodCall call)
        {
            if (!_transport.IsConnected)
                throw new TargetInvocationException("Failed in invoke " +
                                                    call.InterfaceType.Name + "." + call.MethodName,
                                                    new InvalidOperationException("Remoting channel is not open."));


            var request = new Request {Event = new ManualResetEvent(false), Id = call.RequestId};
            _requests.Add(call.RequestId, request);

            _transport.Send(new Packet {Value = request});

            if (!request.Event.WaitOne(5000))
                throw new InvalidOperationException("Failed to get answer from the server.");

            return request.Response;
        }

        private void InvokeMethod(MethodCall call)
        {
            object instance = _serviceResolver.Resolve(call.InterfaceType);

            Response reply;
            if (instance == null)
                reply = new MethodException(call.RequestId,
                                            new InvalidOperationException("Failed to find component on remote end."));
            else
            {
                try
                {
                    MethodInfo method = instance.GetType().GetMethod(call.MethodName);
                    if (method.IsGenericMethod)
                        method = method.MakeGenericMethod(call.GenericArgumentTypes);
                    object result = method.Invoke(instance, call.Arguments);
                    reply = new MethodCallReply(call.RequestId) {Result = result};
                }
                catch (Exception err)
                {
                    reply = new MethodException(call.RequestId,
                                                new TargetInvocationException("Failed to execute method.", err));
                }
            }


            _transport.Send(reply);
        }

        private void OnDisconnect(object sender, EventArgs e)
        {
            if (_reconnectTimer == null)
                _reconnectTimer = new Timer(OnTryReconnect, null, 5000, 5000);
            
        }

        private void OnTryReconnect(object state)
        {
            var timer = _reconnectTimer;
            _reconnectTimer = null;
            timer.Dispose();
            
            try
            {
                var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.Connect(_remoteEndPoint);
                _transport.Start();
            }
            catch(Exception)
            {
                _reconnectTimer = new Timer(OnTryReconnect, null, 5000, 5000);
            }
        }

        private void OnObject(object sender, ObjectEventArgs<Packet> e)
        {
            if (e.Object.Value is MethodCall)
            {
                InvokeMethod((MethodCall) e.Object.Value);
            }
            else if (e.Object.Value is Response)
            {
                var response = (Response) e.Object.Value;
                HandleMethodResponse(response);
            }
        }
    }
}